import sharp from 'sharp';
import fs from 'fs';
import { GlobSync } from 'glob';
import { tools } from './tools/tools';

export namespace image_processor {

    type IMG_NODE = {
        imgInfo?: IMAGE_DATA;
        width: number; //辅助参数
        height: number; //辅助参数
        x: number;
        y: number;
    };

    type POINT_INFO = {
        xSearch: number;
        ySearch: number;
        used: boolean;
    }

    type IMAGE_DATA = {
        name: string;
        width: number;
        height: number;
        rawWidth: number;
        rawHeight: number;
        trimOffsetLeft?: number;
        trimOffsetTop?: number;
        metaData: sharp.Metadata;
        rawData: Buffer;
    };

    type SHEET_FORMAT = {
        file: string;
        frames: {
            [fileName: string]: {
                x: number;
                y: number;
                w: number;
                h: number,
                offX: number;
                offY: number;
                sourceW: number;
                sourceH: number
            }
        };
    }

    export class ImageProcessor {
        constructor(entery: string) {
            this.curRoot = entery;
            this.generalCfg = tools.GenricCfg();
            this.defaultCfg = JSON.parse(fs.readFileSync(this.DefImgCfgPath, {
                encoding: 'utf-8'
            }));
            try {
                this.curHash = JSON.parse(fs.readFileSync(`${this.CfgPath}/${this.generalCfg.img_hash_file}`, {
                    encoding: 'utf-8'
                }));
            } catch (e) {
                this.curHash = {};
            }
        }

        public async Process(): Promise<void>;
        public async Process(path: string, cfg: tools.IMG_SHEET_TYPE): Promise<void>;
        public async Process(path?: string, cfg?: tools.IMG_SHEET_TYPE): Promise<void> {
            if (!cfg) {
                let folders = fs.readdirSync(this.ImgRoot);
                for (let folder of folders) {
                    if (folder == this.defaultCfg.output_dir) {
                        continue;
                    }
                    let globOpts = new GlobSync(`${folder}/**.*`);
                    let hash = this.HashGenerator(globOpts.found);
                    if (hash != this.curHash[folder]) {
                        this.curHash[folder] = hash;
                        await this.ProcessFolder(folder);
                    }
                }
            } else {
                let hash = this.HashGenerator(cfg.include.concat(cfg.cfg_dir));
                let output = `${cfg.output_file}_png`;
                if (hash != this.curHash[output]) {
                    this.curHash[output] = hash;
                    let result = await this.PushImg(cfg.include);
                    if (!!result) {
                        await this.CreateFile(`${path}/${cfg.output_dir}` || '', cfg.output_file, result, cfg.output_cfg || this.defaultCfg.output_cfg);
                    }
                }
            }
            fs.writeFileSync(`${this.CfgPath}/${this.generalCfg.img_hash_file}`, JSON.stringify(this.curHash, null, 4), { encoding: 'utf-8' });
        }

        private HashGenerator(files: string[]): string {
            if (!files || files.length < 1) {
                return '';
            }
            let binBuffers: Buffer[] = [];
            for (let file of files) {
                binBuffers.push(fs.readFileSync(file));
            }
            return tools.Hash(Buffer.concat(binBuffers));
        }

        private async ProcessFolder(folder: string): Promise<void> {

            let curCfg: tools.IMG_ASSEMBLY_CFG | null;
            let curImgRoot = `${this.ImgRoot}/${folder}`;
            try {
                curCfg = JSON.parse(fs.readFileSync(`${curImgRoot}/${this.defaultCfg.cfg_file}`, {
                    encoding: 'utf-8'
                }));

            } catch (e) {
                curCfg = null;
            }

            let glboBase = new GlobSync(`${curImgRoot}/*.png`);
            let result = await this.PushImg(glboBase.found);
            if (!result) {
                return;
            }

            await this.CreateFile(`${this.ImgRoot}/${this.defaultCfg.output_dir}`, folder, result, curCfg?.output_cfg || this.defaultCfg.output_cfg);
        }

        private async CreateFile(outPath: string, fileName: string, imgSet: { width: number, height: number, dic: IMG_NODE[][] }, outputCfg: tools.PNG_OPTIONS) {

            if (imgSet.width > 1024 || imgSet.height > 1024) {
                console.warn(`${fileName}'s ${(imgSet.width > 1024) ? 'width' : 'height'} is larger than 1024`);
            }

            let opts: { overlayOpt: sharp.OverlayOptions[], sheet: SHEET_FORMAT } = this.Format(outPath, imgSet.dic);
            let buffer = await sharp({
                create: {
                    width: imgSet.width,
                    height: imgSet.height,
                    channels: 4,
                    background: { r: 0, b: 0, g: 0, alpha: 0 }
                }
            })
                .composite(opts.overlayOpt)
                // .trim()
                .png(outputCfg || this.defaultCfg.output_cfg)
                .toBuffer();

            tools.CreateFolder(outPath, false);
            fs.writeFileSync(`${outPath}/${fileName}.png`, buffer);
            fs.writeFileSync(`${outPath}/${fileName}.json`, JSON.stringify(opts.sheet, null, 4));
        }

        private Format(folder: string, imgs: IMG_NODE[][]): { overlayOpt: sharp.OverlayOptions[], sheet: SHEET_FORMAT } {
            let tmp: { overlayOpt: sharp.OverlayOptions[], sheet: SHEET_FORMAT } = {
                overlayOpt: [],
                sheet: {
                    file: `${folder}.png`,
                    frames: {}
                }
            };

            for (let strip of imgs) {
                for (let point of strip) {
                    if (point.imgInfo) {
                        tmp.overlayOpt.push({
                            input: point.imgInfo.rawData,
                            left: point.x,
                            top: point.y
                        });
                        tmp.sheet.frames[point.imgInfo.name] = {
                            x: point.x,
                            y: point.y,
                            w: point.width,
                            h: point.height,
                            offX: -(point.imgInfo.trimOffsetLeft || 0),
                            offY: -(point.imgInfo.trimOffsetTop || 0),
                            sourceW: point.imgInfo.rawWidth,
                            sourceH: point.imgInfo.rawHeight
                        }
                    }
                }
            }

            return tmp;
        }

        private async PushImg(paths: string[]): Promise<{ width: number, height: number, dic: IMG_NODE[][] } | void> {
            return new Promise((resolve) => {
                if (!paths || paths.length < 1) {
                    resolve();
                }
                let imgs: IMAGE_DATA[] = [];
                let fileNum: number = paths.length
                for (let path of paths) {
                    fs.readFile(path, null, (err, data) => {
                        if (err) {
                            throw new Error(`can't read file, path: ${path}`);
                        }
                        let matcher = path.match(/\w*\.png$/);
                        let name: string;
                        if (!!matcher) {
                            name = matcher[0];
                        }

                        sharp(data).metadata().then((metadata) => {
                            let node: IMAGE_DATA = {
                                name: name.replace(/.(?=png$)/, '_'),
                                metaData: metadata,
                                rawData: data,
                                rawHeight: metadata.height || 0,
                                rawWidth: metadata.width || 0,
                                height: metadata.height || 0,
                                width: metadata.width || 0
                            };

                            this.CheckSize(data, node).then(() => {
                                imgs.push(node);
                                if (--fileNum < 1) {
                                    imgs.sort((a, b) => {
                                        if (a && b) {
                                            if (a.height == b.height) {
                                                if (a.width == b.width) {
                                                    let hexA = tools.HashNum(a.name);
                                                    let hexB = tools.HashNum(b.name);
                                                    return hexB - hexA;
                                                }
                                                return b.width - a.width;
                                            }
                                            return b.height - a.height;
                                        }
                                        return 0;
                                    });

                                    let rootNode = new ImagePacker(imgs[0].width, imgs[0].height).Merge(imgs);
                                    resolve(rootNode);
                                }
                            })
                        });
                    });
                }
            });
        }

        private async CheckSize(data: Buffer, rawInfo: IMAGE_DATA): Promise<void> {
            if (!data || !rawInfo) {
                return;
            }
            return new Promise<void>(resolve => {
                sharp(data)
                    .trim(1)
                    .toBuffer({ resolveWithObject: true })
                    .then((value) => {
                        rawInfo.rawData = value.data;
                        rawInfo.width = value.info.width || rawInfo.rawWidth;
                        rawInfo.height = value.info.height || rawInfo.rawHeight;
                        rawInfo.trimOffsetLeft = value.info.trimOffsetLeft;
                        rawInfo.trimOffsetTop = value.info.trimOffsetTop;
                        // console.log(`changInfo : name: ${rawInfo.name}\n\twidth: ${rawInfo.rawWidth}->${rawInfo.width}, height: ${rawInfo.rawHeight}->${rawInfo.height}`);
                        resolve();
                    });
            });
        }

        /**
         * 绝对路径
         */
        private PathGenrator(tmpPath: string): string {
            return `${this.curRoot}/${tmpPath}`;
        }

        /**
         * 绝对路径
         */
        private get CfgPath(): string {
            return this.PathGenrator(this.generalCfg.cfg_path);
        }

        /**
         * 绝对路径
         */
        private get DefImgCfgPath(): string {
            return `${this.CfgPath}/${this.generalCfg.img_cfg_file}`;
        }

        /**
         * 绝对路径
         */
        private get ImgRoot(): string {
            return `${this.CfgPath}/${this.defaultCfg.path}`
        }

        private generalCfg: tools.GENERAL_CFG_TYPE;
        private defaultCfg: tools.IMG_ASSEMBLY_CFG;
        private curHash: { [folderName: string]: string };

        private curRoot: string;
    }

    class ImagePacker {

        constructor(width: number, height: number) {
            this.imgSet = [];
            this.pointSet = [];
            this.width = width;
            this.height = height;
        }

        public Merge(imgs: IMAGE_DATA[]): { width: number, height: number, dic: IMG_NODE[][] } | void {
            if (!imgs || imgs.length < 1) {
                return;
            }

            let imgInfo: IMG_NODE = {
                x: 0,
                y: 0,
                imgInfo: imgs[0],
                width: imgs[0].width,
                height: imgs[0].height,

            };
            this.imgSet.push([imgInfo]);

            let firstPt: POINT_INFO = {
                xSearch: imgInfo.width,
                ySearch: 0,
                used: false
            }
            this.pointSet.push([firstPt]);

            for (let i = 1, L = imgs.length; i < L; ++i) {
                let img = imgs[i];
                let strip = 0;

                let IsInserted = false;
                while (strip < this.imgSet.length && !IsInserted) {
                    let point = 0;
                    while (point < this.pointSet[strip].length && !IsInserted) {
                        let ptNode = this.pointSet[strip][point];
                        if (!ptNode.used) {
                            if (this.Check(img, this.imgSet[strip], ptNode)) {
                                IsInserted = true;
                                let texInfo: IMG_NODE = {
                                    x: ptNode.xSearch,
                                    y: ptNode.ySearch,
                                    imgInfo: img,
                                    width: img.width,
                                    height: img.height
                                };

                                ptNode.used = true;

                                let ptTmp1: POINT_INFO = {
                                    xSearch: texInfo.x + texInfo.width,
                                    ySearch: texInfo.y,
                                    used: false
                                };
                                let ptTmp2: POINT_INFO = {
                                    xSearch: texInfo.x,
                                    ySearch: texInfo.y + texInfo.height,
                                    used: false
                                }

                                this.imgSet[strip].push(texInfo);
                                this.pointSet[strip].push(ptTmp1);
                                this.pointSet[strip].push(ptTmp2);

                            }
                        }
                        ++point;
                    }
                    ++strip;
                }
                if (!IsInserted) {
                    let colHeight = 0;
                    let colWidthArr: number[] = [];
                    for (let i = 0, L = this.imgSet.length; i < L; ++i) {
                        colHeight += this.imgSet[i][0].height;
                        colWidthArr[i] = 0;
                        for (let j = 0; j < this.imgSet[i].length; ++j) {
                            colWidthArr[i] += this.imgSet[i][j].width;
                        }
                    }
                    let needHeight = colHeight + img.height;
                    let needWidth = Math.min(...colWidthArr) + img.width;
                    this.height = Math.max(this.height, needHeight); //高度扩张
                    this.width = Math.max(this.width, needWidth);

                    if (needHeight > this.height) {
                        let texInfo: IMG_NODE = {
                            x: 0,
                            y: 0,
                            imgInfo: img,
                            width: img.width,
                            height: img.height
                        };

                        let ptTmp1: POINT_INFO = {
                            xSearch: texInfo.width,
                            ySearch: 0,
                            used: false
                        };
                        let ptTmp2: POINT_INFO = {
                            xSearch: 0,
                            ySearch: this.imgSet[this.imgSet.length - 1][0].height + this.imgSet[this.imgSet.length - 1][0].y,
                            used: false
                        }

                        this.imgSet.push([texInfo]);
                        this.pointSet.push([ptTmp1]);
                        this.pointSet[this.pointSet.length - 1].push(ptTmp2);
                    } else {
                        let texInfo: IMG_NODE = {
                            x: 0,
                            y: this.imgSet[this.imgSet.length - 1][0].height + this.imgSet[this.imgSet.length - 1][0].y,
                            imgInfo: img,
                            width: img.width,
                            height: img.height
                        };

                        let ptTmp: POINT_INFO = {
                            xSearch: texInfo.width,
                            ySearch: texInfo.y,
                            used: false
                        };

                        this.imgSet.push([texInfo]);
                        this.pointSet.push([ptTmp]);
                    }
                }
            }

            return { width: this.width, height: this.height, dic: this.imgSet };
        }

        private Check(img: IMAGE_DATA, tex: IMG_NODE[], point: POINT_INFO/* , lastStrip: boolean */): boolean {
            let maxY: number = tex[0].y + tex[0].height;

            let tileW = img.width || 0;
            let tileH = img.height || 0;

            if (point.xSearch + tileW > this.width || point.ySearch + tileH > maxY) {
                return false;
            }

            if (point.xSearch + tileW > this.width) {
                this.width += tileW;
            }

            if (point.ySearch + tileH > maxY) {
                this.height += tileH;
            }

            let strip = true;
            for (let _tex of tex) {
                if (((_tex.x + _tex.width) > point.xSearch) && (_tex.y < (point.ySearch + tileH))
                    && (_tex.x < (point.xSearch + tileW)) && ((_tex.y + _tex.height) > point.ySearch)) {
                    strip = false;
                    break;
                }
            }
            return strip;
        }

        private imgSet: IMG_NODE[][];
        private pointSet: POINT_INFO[][];
        private width: number;
        private height: number;

    }
}